package com.aiwiown.snackmq.storage.index;

import com.aiwiown.snackmq.storage.config.StorageConfig;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

@Slf4j
public class IndexManager {
    private final String indexPath;
    private final StorageConfig config;
    private final ConcurrentHashMap<String, IndexFile> indexFiles;
    private final AtomicLong currentIndexId;
    private IndexFile currentIndexFile;

    public IndexManager(StorageConfig config) {
        this.config = config;
        this.indexPath = config.getBaseDir() + "/index";
        this.indexFiles = new ConcurrentHashMap<>();
        this.currentIndexId = new AtomicLong(0);
    }

    public void init() throws IOException {
        File dir = new File(indexPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        File[] files = dir.listFiles((d, name) -> name.endsWith(".idx"));
        if (files != null) {
            for (File file : files) {
                if (file.isFile()) {
                    IndexFile indexFile = new IndexFile(file.getName(), config);
                    indexFiles.put(file.getName(), indexFile);
                }
            }
        }

        if (indexFiles.isEmpty()) {
            // If no files exist, start with ID 0.
            // createNewIndexFile will use ID 0 and then set the next ID to 1.
            createNewIndexFile();
        } else {
            String latestFileName = getLatestIndexFile();
            currentIndexFile = indexFiles.get(latestFileName);
            // **BUG FIX**: Correctly set the *next* available ID to be one greater
            // than the ID of the most recent index file.
            long latestId = Long.parseLong(latestFileName.substring(0, latestFileName.length() - 4));
            currentIndexId.set(latestId + 1);
        }
        log.info("IndexManager initialized. Current active index file: {}. Next file ID will be {}.",
                currentIndexFile.getFileName(), currentIndexId.get());
    }

    private void createNewIndexFile() throws IOException {
        // This method now correctly uses the next available ID for the new file name.
        String fileName = String.format("%020d.idx", currentIndexId.get());
        IndexFile indexFile = new IndexFile(fileName, config);
        indexFiles.put(fileName, indexFile);
        currentIndexFile = indexFile;
        // Increment the ID to prepare for the *next* roll.
        currentIndexId.incrementAndGet();
        log.info("Rolled to new index file: {}", fileName);
    }

    private String getLatestIndexFile() {
        return indexFiles.keySet().stream()
                .map(fileName -> Long.parseLong(fileName.substring(0, fileName.length() - 4)))
                .max(Long::compareTo)
                .map(fileName -> String.format("%020d.idx", fileName))
                .orElseThrow(() -> new IllegalStateException("No index files found, but the indexFiles map was not empty."));
    }

    /**
     * Adds an index entry, handling file rolling if necessary.
     */
    public void addIndex(IndexEntry entry) throws IOException {
        // Use double-checked locking for performance to avoid synchronizing on every write.
        try {
            if (currentIndexFile.getFileSize() >= config.getMaxFileSize()) {
                synchronized (this) {
                    // Re-check inside the synchronized block to prevent race conditions.
                    if (currentIndexFile.getFileSize() >= config.getMaxFileSize()) {
                        createNewIndexFile();
                    }
                }
            }
            currentIndexFile.addIndex(entry);
        } catch (Exception e) {
            log.error("Failed to add index for message: {}", entry.getMessageId(), e);
            // 如果是通道关闭错误，记录但不抛出异常，避免影响批量写入
            if (e.getCause() instanceof java.nio.channels.ClosedChannelException) {
                log.warn("Index file channel is closed, skipping index creation for message: {}", entry.getMessageId());
                return;
            }
            throw new IOException("Failed to add index", e);
        }
    }

    /**
     * Gets an index entry by message ID by searching across all index files.
     */
    public IndexEntry getIndex(String messageId) {
        if (messageId == null || messageId.isEmpty()) {
            log.warn("Message ID is null or empty");
            return null;
        }
        try {
            // Search from the newest file to the oldest for better performance on recent messages.
            List<IndexFile> sortedFiles = new ArrayList<>(indexFiles.values());
            sortedFiles.sort((f1, f2) -> f2.getFileName().compareTo(f1.getFileName()));

            for (IndexFile indexFile : sortedFiles) {
                IndexEntry entry = indexFile.getIndex(messageId);
                if (entry != null) {
                    return entry;
                }
            }
            log.debug("Index entry not found for message: {}", messageId);
            return null;
        } catch (Exception e) {
            log.error("Error getting index for message: {}", messageId, e);
            return null;
        }
    }

    /**
     * Removes an index entry from memory.
     * Note: This does not remove the entry from the disk file.
     */
    public void removeIndex(String messageId) {
        for (IndexFile indexFile : indexFiles.values()) {
            indexFile.removeIndex(messageId);
        }
    }

    /**
     * Gets index entries within a time range.
     */
    public List<IndexEntry> getIndexesByTimeRange(long startTime, long endTime) {
        List<IndexEntry> result = new ArrayList<>();
        for (IndexFile indexFile : indexFiles.values()) {
            indexFile.getAllIndexes().values().stream()
                    .filter(entry -> entry.getTimestamp() >= startTime && entry.getTimestamp() <= endTime)
                    .forEach(result::add);
        }
        return result;
    }
    
    /**
     * 根据逻辑偏移量查找物理位置
     * 
     * @param logicalOffset 逻辑偏移量
     * @return 物理位置信息，包含文件索引和文件内偏移量
     */
    public PhysicalPosition findPhysicalPosition(long logicalOffset) {
        if (logicalOffset < 0) {
            log.warn("Invalid logical offset: {}", logicalOffset);
            return null;
        }
        
        try {
            // 计算文件索引和文件内偏移量
            long mappedFileSize = config.getMappedFileSize();
            int fileIndex = (int) (logicalOffset / mappedFileSize);
            long fileOffset = logicalOffset % mappedFileSize;
            
            log.debug("Logical offset {} maps to fileIndex={}, fileOffset={}", 
                    logicalOffset, fileIndex, fileOffset);
            
            return new PhysicalPosition(fileIndex, fileOffset);
        } catch (Exception e) {
            log.error("Error finding physical position for logical offset: {}", logicalOffset, e);
            return null;
        }
    }
    
    /**
     * 添加逻辑偏移量到物理位置的映射
     * 
     * @param logicalOffset 逻辑偏移量
     * @param fileIndex 文件索引
     * @param fileOffset 文件内偏移量
     */
    public void append(long logicalOffset, int fileIndex, long fileOffset) {
        try {
            // 创建索引条目
            IndexEntry entry = new IndexEntry(
                String.valueOf(logicalOffset), // 使用逻辑偏移量作为消息ID
                fileIndex,
                fileOffset,
                (int) fileOffset, // position 参数
                System.currentTimeMillis()
            );
            
            // 添加到索引文件
            addIndex(entry);
            
            log.debug("Added index mapping: logicalOffset={} -> fileIndex={}, fileOffset={}", 
                    logicalOffset, fileIndex, fileOffset);
        } catch (Exception e) {
            log.error("Failed to append index for logicalOffset={}, fileIndex={}, fileOffset={}", 
                    logicalOffset, fileIndex, fileOffset, e);
        }
    }
    
    /**
     * 物理位置信息
     */
    public static class PhysicalPosition {
        private final int fileIndex;
        private final long fileOffset;
        
        public PhysicalPosition(int fileIndex, long fileOffset) {
            this.fileIndex = fileIndex;
            this.fileOffset = fileOffset;
        }
        
        public int getFileIndex() {
            return fileIndex;
        }
        
        public long getFileOffset() {
            return fileOffset;
        }
        
        @Override
        public String toString() {
            return "PhysicalPosition{fileIndex=" + fileIndex + ", fileOffset=" + fileOffset + "}";
        }
    }

    public void close() {
        for (IndexFile indexFile : indexFiles.values()) {
            indexFile.close();
        }
        indexFiles.clear();
    }

    public int getIndexFileCount() {
        return indexFiles.size();
    }

    public long getTotalIndexCount() {
        return indexFiles.values().stream()
                .mapToLong(IndexFile::getIndexCount)
                .sum();
    }

    public Map<String, IndexEntry> getAllIndexes() {
        Map<String, IndexEntry> allIndexes = new ConcurrentHashMap<>();
        for (IndexFile indexFile : indexFiles.values()) {
            allIndexes.putAll(indexFile.getAllIndexes());
        }
        return allIndexes;
    }

    /**
     * Closes all file handles and deletes all index files and the directory.
     */
    public void delete() {
        try {
            close();
            File indexDir = new File(indexPath);
            if (indexDir.exists()) {
                File[] files = indexDir.listFiles();
                if (files != null) {
                    // **BUG FIX**: Completed the for-loop to delete all files.
                    for (File file : files) {
                        if (!file.delete()) {
                            log.warn("Failed to delete index file: {}", file.getAbsolutePath());
                        }
                    }
                }
                if (!indexDir.delete()) {
                    log.warn("Failed to delete index directory: {}", indexPath);
                }
            }
        } catch (Exception e) {
            log.error("Error during index manager deletion", e);
        }
    }
}